package commons.utils;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.HashMap;

/**
 * Miscellaneous formatting utility methods.
 */
public class FormatUtils {
    public final static DecimalFormat DECIMAL_FORMAT = new DecimalFormat("###,###,###.00");
    public final static DecimalFormat DECIMAL_FORMAT_4 = new DecimalFormat("###,###,##0.####");
    public static final DecimalFormat NUMBER_FORMAT = new DecimalFormat("#########0.00");
    public final static int DEFAULT_ROUND_SCALE = 3;

    /**
     * A formatter for use on reports. Zeros are formatted as empty strings.
     */
    public static final Format REPORT_DECIMAL_FORMAT = new Format() {
	private static final long serialVersionUID = 1L;

	@Override
	public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
	    if (obj == null) {
		return toAppendTo;
	    }
	    if (obj instanceof Number) {
		double d = ((Number) obj).doubleValue();
		if (!(d < -0.0 || 0.0 < d)) {
		    return toAppendTo;
		}
	    }
	    return DECIMAL_FORMAT.format(obj, toAppendTo, pos);
	}

	@Override
	public Object parseObject(String source, ParsePosition pos) {
	    return DECIMAL_FORMAT.parseObject(source, pos);
	}
    };

    /**
     * A formatter for use on grid viewers for CORE codes. Will change string to
     * title case.
     */
    public static final Format CORE_CODES_FORMAT = new Format() {
	private static final long serialVersionUID = 1L;

	@Override
	public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
	    if (obj == null) {
		return toAppendTo;
	    }
	    String s = obj.toString();
	    if (s.length() == 0) {
		return toAppendTo;
	    }
	    return toAppendTo.append(s.charAt(0)).append(s.length() > 1 ? s.toLowerCase().substring(1) : "");
	}

	@Override
	public Object parseObject(String source, ParsePosition pos) {
	    return source.toUpperCase();
	}
    };
    public static final Format REPORT_DATE_FORMAT = new SimpleDateFormat("MM/dd/yyyy");
    public static final Format CREATE_DATE_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");

    public static Format getReportDateFormat() {
	return REPORT_DATE_FORMAT;
    }

    public static Format getReportMoneyFormat() {
	return REPORT_DECIMAL_FORMAT;
    }

    /**
     * A formatter for use on reports. Zeros are formatted as empty strings. Up
     * to 10 digits to the left of the decimal
     */
    public static final Format LONG_PERCENT_DECIMAL_FORMAT = new Format() {
	private static final long serialVersionUID = 1L;

	@Override
	public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
	    if (obj == null) {
		return toAppendTo;
	    }
	    if (obj instanceof Number) {
		double d = ((Number) obj).doubleValue();
		if (!(d < -0.0 || 0.0 < d)) {
		    return toAppendTo;
		}
	    }
	    return __longPercentFormat.format(obj, toAppendTo, pos);
	}

	@Override
	public Object parseObject(String source, ParsePosition pos) {
	    return __longPercentFormat.parseObject(source, pos);
	}
    };

    private static DecimalFormat __longPercentFormat = new DecimalFormat("###,###,###.#########################");
    private static final String __zeros = "00000000000000000000000000000000000000";
    private static HashMap<String, String> __map = new HashMap<String, String>();
    static {
	__map.put("0", "Zero");
	__map.put("01", "One");
	__map.put("1", "One");
	__map.put("2", "Two");
	__map.put("3", "Three");
	__map.put("4", "Four");
	__map.put("5", "Five");
	__map.put("6", "Six");
	__map.put("7", "Seven");
	__map.put("8", "Eight");
	__map.put("9", "Nine");
	__map.put("00", "Zero");
	__map.put("01", "One");
	__map.put("02", "Two");
	__map.put("03", "Three");
	__map.put("04", "Four");
	__map.put("05", "Five");
	__map.put("06", "Six");
	__map.put("07", "Seven");
	__map.put("08", "Eight");
	__map.put("09", "Nine");
	__map.put("10", "Ten");
	__map.put("11", "Eleven");
	__map.put("12", "Twelve");
	__map.put("13", "Thirteen");
	__map.put("14", "Fourteen");
	__map.put("15", "Fifteen");
	__map.put("16", "Sixteen");
	__map.put("17", "Seventeen");
	__map.put("18", "Eighteen");
	__map.put("19", "Nineteen");
	__map.put("20", "Twenty");
	__map.put("30", "Thirty");
	__map.put("40", "Forty");
	__map.put("50", "Fifty");
	__map.put("60", "Sixty");
	__map.put("70", "Seventy");
	__map.put("80", "Eighty");
	__map.put("90", "Ninety");
    }

    /**
     * returns the given number as human readable text suitable for printing on
     * a bank check.
     * 
     * For instance, 124.45 returns "One Hundred Forty Five and 45/100".
     * 
     * Only supports numbers less than 999,999,999.99 and greater than
     * -999,999,999.99.
     */
    public static final String formatAsText(double number) {
	String result = "";

	if (number > 999999999.99) {
	    throw new IllegalArgumentException("Numbers greater than 999,999,999 are not supported");
	}
	if (number < -999999999.99) {
	    throw new IllegalArgumentException("Numbers less than -999,999,999 are not supported");
	}

	if (number <= 0.0) {
	    if (number >= 0.0) {
		return "Zero and 00/100";
	    }
	    result = "Negative ";
	    number *= -1;
	}

	String s = StringUtils.replaceAll(formatNumber(number, 2), ",", "");
	String dollars = s;
	String cents = s;
	int point = s.indexOf('.');
	if (0 <= point) {
	    dollars = s.substring(0, point);
	    cents = s.substring(point + 1);
	}
	if (dollars.length() >= 7) {
	    result += formatWholeNumber(Integer.parseInt(dollars) / 1000000) + " Million ";
	    dollars = StringUtils.tail(dollars, 6);
	}
	if (dollars.length() >= 4) {
	    String temp = formatWholeNumber(Integer.parseInt(dollars) / 1000);
	    if (0 < temp.length()) {
		result += formatWholeNumber(Integer.parseInt(dollars) / 1000) + " Thousand ";
	    }
	    dollars = StringUtils.tail(dollars, 3);
	}
	String temp = formatWholeNumber(Integer.parseInt(dollars));
	if (0 < temp.length()) {
	    result += temp;
	}
	if (0 < result.length()) {
	    result += " and ";
	}
	if (cents.length() > 0) {
	    result += cents + "/100";
	} else {
	    result += "00/100";
	}
	result = StringUtils.replaceAll(result, "  ", " ");
	return result.trim();
    }

    /**
     * formats a number > 0 and <= 999
     */
    private static String formatWholeNumber(int i) {
	if (i <= 0) {
	    return "";
	}
	String result = "";
	String s = Integer.toString(i);
	if (3 <= s.length()) {
	    result += __map.get(s.subSequence(0, 1)) + " Hundred ";
	    s = s.substring(1);
	}
	if (2 <= s.length() && 20 <= Integer.parseInt(s)) {
	    result += __map.get(s.subSequence(0, 1) + "0") + " ";
	    s = s.substring(1);
	}
	String tail = StringUtils.tail(s, 2);
	if (!tail.equals("0") && !tail.equals("00")) {
	    result += __map.get(tail);
	}
	return result.trim();
    }

    public static String formatNumber(double value, int decimalPlaces) {
	if (Double.isInfinite(value) || Double.isNaN(value)) {
	    return "";
	}
	String zeros = __zeros.substring(0, decimalPlaces);
	String lResult = new BigDecimal(Double.toString(value)).setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP).toString();
	int iPoint = lResult.indexOf('.');
	if (iPoint < 0) {
	    return lResult + "." + zeros;
	}
	return (lResult + zeros).substring(0, iPoint + decimalPlaces + 1);
    }

    /**
     * Method can be used if text representation of a double (e.g
     * "1,234,678.001") has to be rounded to n decimals. For example
     * roundDouble("1,234.56789", 2) returns 1,234.57
     * 
     * @param value
     * @param decimalPlaces
     * @return
     */
    public static String formatDouble(Object value, int decimalPlaces) {
	double dValue = ConversionUtils.toSafeDouble(value, 0.0);
	return formatNumber(dValue, decimalPlaces);
    }
}
